'use strict';

/* Built-in Modules */
var path = require('path');
var URL  = require('url').URL;

/* 3rd-party Modules */
var fs            = require('fs-extra');
var async         = require('async');
var yaml          = require('js-yaml');
var markdownTable = require('markdown-table');
var stringWidth   = require('string-width');
var simpleGit     = require('simple-git');
var sortedJSON    = require('sorted-json');
var request       = require('request');

/* Project Modules */
var E            = require('../utils/serverError');
var CONFIG       = require('../utils/yamlResources').get('CONFIG');
var toolkit      = require('../utils/toolkit');
var common       = require('../utils/common');

var scriptSetAPICtrl = require('./scriptSetAPICtrl');
var scriptMarketMod  = require('../models/scriptMarketMod');
var scriptSetMod     = require('../models/scriptSetMod');
var scriptMod        = require('../models/scriptMod');
var funcMod          = require('../models/funcMod');
var cronJobMod       = require('../models/cronJobMod');

/* Init */
var AUTO_GENERATION_INFO_TEXT = 'This file is auto generated by DataFlux Func';

var OSSUTIL_CMD = path.join(__dirname, `../../tools/${process.arch === 'x64' ? 'ossutil64' : 'ossutilarm64'}`);

var SCRIPT_MARKET_RW_MAP = {
  git        : 'rw',
  aliyunOSS  : 'rw',
  httpService: 'ro',
}

var EXPORT_FIELDS = [
  'exportUser',
  'exportTime',
  'note',
]

function _prepareConfig(data) {
  if (toolkit.isNothing(data.configJSON)) return data;

  ['url', 'endpoint', 'folder'].forEach(function(f) {
    if (data.configJSON[f]) {
      data.configJSON[f] = data.configJSON[f].replace(/\/*$/g, '').replace(/^\/*/g, '');
    }
  });

  return data;
};

function _checkConfig(locals, data, callback) {
  var type   = data.type;
  var config = data.configJSON;

  var requiredFields = [];
  var optionalFields = [];

  switch(type) {
    case 'git':
      requiredFields = ['url'];
      optionalFields = ['branch', 'user', 'password'];
      break;

    case 'aliyunOSS':
      requiredFields = ['endpoint', 'bucket', 'folder'];
      optionalFields = ['accessKeyId', 'accessKeySecret'];
      break;

    case 'httpService':
      requiredFields = ['url'];
      optionalFields = [];
      break;
  }

  // Check fields
  for (var i = 0; i < requiredFields.length; i++) {
    var f = requiredFields[i];

    if ('undefined' === typeof config[f]) {
      return callback(new E('EClientBadRequest.InvalidScriptMarketAuthJSON', 'Invalid config JSON', {
        requiredFields: requiredFields,
        optionalFields: optionalFields,
        missingField  : f,
      }));
    }
  }

  // TODO Try to connect to Script Market to verify config
  return callback();
};

function _getGitRepoAuthURL(scriptMarket, masked) {
  var config = scriptMarket.configJSON || {};
  var urlObj = new URL(config.url);
  if (masked) {
    urlObj.username = '';
    urlObj.password = '';
  } else {
    urlObj.username = config.user     || 'anonymity';
    urlObj.password = config.password || 'anonymity';
  }

  return urlObj.toString();
};

function _maskGitConfig(localPath, scriptMarket, callback) {
  var gitconfigPath = path.join(localPath, '.git/config');
  var gitconfig     = toolkit.safeReadFileSync(gitconfigPath);

  var maskedGitconfig = gitconfig.replace(`url = ${_getGitRepoAuthURL(scriptMarket)}`, `url = ${_getGitRepoAuthURL(scriptMarket, true)}`);

  fs.outputFile(gitconfigPath, maskedGitconfig, callback);
};

function _unmaskGitConfig(localPath, scriptMarket, callback) {
  var gitconfigPath   = path.join(localPath, '.git/config');
  var maskedGitconfig = toolkit.safeReadFileSync(gitconfigPath);

  var gitconfig = maskedGitconfig.replace(`url = ${_getGitRepoAuthURL(scriptMarket, true)}`, `url = ${_getGitRepoAuthURL(scriptMarket)}`);

  fs.outputFile(gitconfigPath, gitconfig, callback);
};

function _getToken(scriptMarket) {
  if (!scriptMarket.id) {
    throw new Error('No Script Market ID');
  }

  return toolkit.getStringSign(scriptMarket.id);
};

function _getRemoteTokenInfo(scriptMarket) {
  var localPath = _getLocalAbsPath(scriptMarket);

  var tokenFilePath = path.join(localPath, CONFIG._SCRIPT_MARKET_TOKEN_FILE);
  var remoteToken   = toolkit.safeReadFileSync(tokenFilePath).trim();

  var info = {
    path : tokenFilePath,
    value: remoteToken,
  }
  return info;
};

function _addAutoGeneratedInfo(text, style) {
  var infoLine = '';
  switch(style) {
    case 'html':
    case 'markdown':
      infoLine = `<!-- ${AUTO_GENERATION_INFO_TEXT} -->`;
      break;

    case 'yaml':
    case 'python':
      infoLine = `# ${AUTO_GENERATION_INFO_TEXT}`;
      break;

    default:
      infoLine = AUTO_GENERATION_INFO_TEXT;
      break;
  }
  return [ infoLine, text || '' ].join('\n\n');
};

function _addChangelogInfo(changelogInfo, scriptSet) {
  if (toolkit.isNothing(changelogInfo)) {
    changelogInfo = {
      title     : 'Changelog / 变更日志',
      changelogs: [],
    }
  }

  var scriptSetExtra = scriptSet._extra || [];

  changelogInfo.changelogs.push({
    time: scriptSetExtra.exportTime || null,
    by  : scriptSetExtra.exportUser || null,
    note: scriptSetExtra.note       || null,
  });

  return changelogInfo;
};

function _isAutoGeneratedFileOrNothing(filePath) {
  if (!fs.existsSync(filePath)) return true;

  var text = toolkit.safeReadFileSync(filePath).trim();
  if (toolkit.isNothing(text) || text.split('\n')[0].indexOf(AUTO_GENERATION_INFO_TEXT) >= 0) {
    return true;
  }

  return false;
};

function _getMarkdownTable(body) {
  return markdownTable(body, { stringLength: stringWidth });
};

function _getDefaultScriptMarketReadmeContent(scriptMarket, pushContent) {
  // Script Market info
  var content = [
    `# Script Market / 脚本市场${scriptMarket.name ? ' - ' + scriptMarket.name : ''}`,
    `${toolkit.toMarkdownTextBlock(scriptMarket.description) || '*No description*<br>*没有具体描述*'}`,
  ];

  var pushExtra = pushContent.extra || {};

  // Recent activities
  if (toolkit.notNothing(pushContent.deleteScriptSets)) {
    content.push(`<br>`,
                  `## 1. Recent Deleted / 最近删除`);

    if (pushExtra.note) {
      content.push(toolkit.toMarkdownTextBlock(pushExtra.note));
    }

    var _table = [
      [
        '#',
        'ID',
        'Title<br>标题',
        'Directory<br>目录',
        'Delete Time<br>删除时间',
      ],
    ];
    var deleteTime = toolkit.getISO8601();
    pushContent.deleteScriptSets.forEach(function(scriptSet, index) {
      _table.push([
        `${index + 1}`,
        `\`${scriptSet.id}\``,
        `${scriptSet.title || '-'}`,
        `${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}`,
        deleteTime,
      ])
    });

    content.push(_getMarkdownTable(_table));
  }

  if (toolkit.notNothing(pushContent.scriptSets)) {
    content.push(`<br>`,
                  `## 1. Recent Published / 最近发布`);

    if (pushExtra.note) {
      content.push(toolkit.toMarkdownTextBlock(pushExtra.note));
    }

    var _table = [
      [
        '#',
        'ID',
        'Title<br>标题',
        'Directory<br>目录',
        'Publisher<br>发布者',
        'Changelog<br>变更日志',
      ],
    ];
    pushContent.scriptSets.forEach(function(scriptSet, index) {
      var exportTime = pushExtra.exportTime ? toolkit.getISO8601(pushExtra.exportTime) : '-';
      _table.push([
        `${index + 1}`,
        `\`${scriptSet.id}\``,
        `${scriptSet.title || '-'}`,
        `[${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}](${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id})`,
        `${pushExtra.exportUser || '-'}<br>${exportTime}`,
        `[Show / 查看](${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}/CHANGELOG.md)`,
      ])
    });

    content.push(_getMarkdownTable(_table));
  }

  // Script Set Overview
  var scriptSets = _getMetaData(scriptMarket).scriptSets || [];
  if (scriptSets.length > 0) {
    content.push(`<br>`,
                  `## 2. Overview / 总览`,
                  `This Script Market contains the following Scripts Sets:<br>此脚本市场包含以下脚本集：`);

    var _table = [
      [
        '#',
        'ID',
        'Title<br>标题',
        'Directory<br>目录',
        'Publisher<br>发布者',
        'Changelog<br>变更日志',
      ],
    ];
    scriptSets.forEach(function(scriptSet, index) {
      var scriptSetExtra = scriptSet._extra || {};
      var exportTime = scriptSetExtra.exportTime ? toolkit.getISO8601(scriptSetExtra.exportTime) : '-';
      _table.push([
        `${index + 1}`,
        `\`${scriptSet.id}\``,
        `${scriptSet.title || '-'}`,
        `[${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}](${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id})`,
        `${scriptSetExtra.exportUser || '-'}<br>${exportTime}`,
        `[Show / 查看](${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}/CHANGELOG.md)`,
      ]);
    });
    content.push(_getMarkdownTable(_table));

  } else {
    content.push(`<br>`,
                  `## 2. Overview / 总览`,
                  `This Script Market does not contains any Scripts Set:<br>此脚本市场暂不包含任何脚本集：`);
  }

  return _addAutoGeneratedInfo(content.join('\n\n'), 'markdown');
};

function _getDefaultScriptSetReadmeContent(scriptSet) {
  var scriptSetExtra = scriptSet._extra || {};
  var exportTime = scriptSetExtra.exportTime ? toolkit.getISO8601(scriptSetExtra.exportTime) : '-';

  // Script Set info
  var content = [
    `# ${scriptSet.title || scriptSet.id}`,
    _getMarkdownTable([
      [
        'ID',
        'Title<br>标题',
        'Publisher<br>发布者',
        'Changelog<br>变更日志',
      ],
      [
        `\`${scriptSet.id}\``,
        `${scriptSet.title || '-'}`,
        `${scriptSetExtra.exportUser || '-'}<br>${exportTime}`,
        `[Show / 查看](CHANGELOG.md)`,
      ],
    ]),

    `<br>`,
    `## 1. Description / 描述`,
    `${toolkit.toMarkdownTextBlock(scriptSet.description) || '*No description*<br>*没有具体描述*'}`,

    `<br>`,
    `## 2. Dependency / 依赖包`,
    toolkit.isNothing(scriptSet.requirements)
      ? `*No dependency required*<br>*不需要任何依赖包*`
      : `~~~text\n${scriptSet.requirements.trim()}\n~~~`,
  ];

  if (toolkit.notNothing(scriptSet.scripts)) {
    // Script info
    content.push(`<br>`,
                  `## 3. Scripts / 脚本`,
                  `This Script Set contains the following Scripts:<br>此脚本集包含以下脚本：`);

    var _table = [
      [
        '#',
        'ID',
        'Title<br>标题',
        'Lines<br>行数',
        'Update Time<br>更新时间',
      ],
    ];
    var codeLines = 0;
    scriptSet.scripts.forEach(function(s, index) {
      // File name
      var filename = common.getScriptFilename(s);

      // Code lines
      var lines = 0;
      if ('string' === typeof s.code) {
        lines = s.code.split('\n').length;
        codeLines += lines;
      }

      // Update time
      var updateTime = s.updateTime ? toolkit.getISO8601(s.updateTime) : '-';

      _table.push([
        `${index + 1}`,
        `[\`${s.id}\`](${filename})`,
        `${s.title || '-'}`,
        lines,
        updateTime,
      ]);
    });
    content.push(_getMarkdownTable(_table));

    // Statistic
    content.push(`<br>`,
                  `## 4. Statistic / 统计`,
                  `The statistic of this Script Set are as follows:<br>此脚本集统计信息如下：`);

    content.push(_getMarkdownTable([
      [ 'Item<br>项目', 'Result<br>结果' ],
      [ 'Script Count<br>脚本数量', scriptSet.scripts.length ],
      [ 'Total Lines<br>脚本总行数', codeLines ],
    ]));
  }

  return _addAutoGeneratedInfo(content.join('\n\n'), 'markdown');
};

function _getDefaultScriptSetChangelogContent(changelogInfo) {
  var content = [
    `# ${changelogInfo.title}`,
  ]
  for (var i = changelogInfo.changelogs.length - 1; i >= 0; i--) {
    var changelog = changelogInfo.changelogs[i];

    content.push(`<br>`,
                  `## ${toolkit.getISO8601(changelog.time || changelog.date)}`,
                  '<br>',
                  `> BY ${changelog.by}`);
    content.push(toolkit.toMarkdownTextBlock(changelog.note));
  };

  return _addAutoGeneratedInfo(content.join('\n\n'), 'markdown');
};

// Script Market - Init
var SCRIPT_MARKET_INIT_FUNC_MAP = {
  git: function(locals, scriptMarket, setAdmin, callback) {
    var gitURL       = _getGitRepoAuthURL(scriptMarket);
    var localPath    = _getLocalAbsPath(scriptMarket);
    var localPathTmp = `${localPath}.tmp`;

    var branch = 'master';
    if (scriptMarket.configJSON && scriptMarket.configJSON.branch) {
      branch = scriptMarket.configJSON.branch;
    }

    // Clear folder
    fs.emptyDirSync(localPathTmp);

    // Run git
    var isRemoteExists = null;
    var git = toolkit.createGitHandler(localPathTmp);
    async.series([
      // git clone
      function(asyncCallback) {
        git.clone(gitURL, localPathTmp, asyncCallback);
      },
      // git branch
      function(asyncCallback) {
        git.branch('--all', function(err, gitRes) {
          if (err) return asyncCallback(err);

          // Check if remote branch exists or not
          isRemoteExists = gitRes.all.indexOf(`remotes/origin/${branch}`) >= 0;

          return asyncCallback();
        })
      },
      // git checkout
      function(asyncCallback) {
        if (isRemoteExists) {
          // Checkout directly if remote branch already exists
          return git.checkout(branch, asyncCallback);

        } else if (setAdmin) {
          // Create and push branch if no matched remote branch when operate as admin
          return async.series([
            // git checkout
            function(innerCallback) {
              git.checkout([ '-B', branch ], innerCallback);
            },
            // git add
            function(innerCallback) {
              // Create blank README.md file
              var scriptMarketReadmeFilePath = path.join(localPathTmp, CONFIG._SCRIPT_MARKET_README_FILE);
              var scriptMarketReadmeData = _addAutoGeneratedInfo(null, 'markdown');
              fs.outputFileSync(scriptMarketReadmeFilePath, scriptMarketReadmeData);

              git.add('.', innerCallback);
            },
            // git commit
            function(innerCallback) {
              git
              .addConfig('user.name', locals.user.name)
              .addConfig('user.email', locals.user.email)
              .commit('Init Script Market', innerCallback);
            },
            // git push --set-upstream
            function(innerCallback) {
              git.push(['--set-upstream', 'origin', branch], innerCallback);
            }
          ], asyncCallback);

        } else {
          // Error if no matched remote branch
          return asyncCallback(new E('EBizCondition.NoSuchBranch', 'No such branch in the git repo'));
        }
      },
      // Mask password
      function(asyncCallback) {
        _maskGitConfig(localPathTmp, scriptMarket, asyncCallback);
      },
    ], function(err) {
      if (err) {
        fs.removeSync(localPathTmp);

        if (err instanceof E) {
          return callback(err);
        } else {
          var _message = err.toString().trim().split('\n').pop();
          return callback(new E('EClient', 'Failed to access git repo', { message: _message }));
        }
      }

      fs.moveSync(localPathTmp, localPath, { overwrite: true });
      fs.removeSync(localPathTmp);
      return callback();
    });
  },
  aliyunOSS: function(locals, scriptMarket, setAdmin, callback) {
    var localPath    = _getLocalAbsPath(scriptMarket);
    var localPathTmp = `${localPath}.tmp`;

    // Clear folder
    fs.emptyDirSync(localPathTmp);

    async.series([
      // Download META file
      function(asyncCallback) {
        SCRIPT_MARKET_DOWNLOAD_FUNC_MAP[scriptMarket.type](scriptMarket, localPathTmp, CONFIG._SCRIPT_EXPORT_META_FILE, asyncCallback);
      },
      // Download TOKEN file
      function(asyncCallback) {
        SCRIPT_MARKET_DOWNLOAD_FUNC_MAP[scriptMarket.type](scriptMarket, localPathTmp, CONFIG._SCRIPT_MARKET_TOKEN_FILE, function(err) {
          // Ignore error if downloading TOKEN file fails
          return asyncCallback();
        });
      },
    ], function(err) {
      if (err) {
        fs.removeSync(localPathTmp);
        return callback(new E('EClient', 'Init Script Market failed', { message: err.toString() }));
      }

      fs.moveSync(localPathTmp, localPath, { overwrite: true });
      fs.removeSync(localPathTmp);
      return callback();
    });
  },
};
SCRIPT_MARKET_INIT_FUNC_MAP.httpService = SCRIPT_MARKET_INIT_FUNC_MAP.aliyunOSS;

// Script Market - Reset
var SCRIPT_MARKET_RESET_FUNC_MAP = {
  git: function(locals, scriptMarket, callback) {
    var localPath = _getLocalAbsPath(scriptMarket);
    if (!fs.existsSync(path.join(localPath, '.git/config'))) {
      return callback(new Error('Local git folder is broken, please delete the Script Market and add it again'))
    }

    var git = toolkit.createGitHandler(localPath);

    var prevCommitId = null;

    var lockKey     = toolkit.getCacheKey('lock', 'scriptMarketOperation');
    var lockValue   = toolkit.genRandString();
    var lockAge     = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_AGE;
    var lockWaitAge = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_WAIT_AGE;
    async.series([
      // Lock
      function(asyncCallback) {
        locals.cacheDB.lockWait(lockKey, lockValue, lockAge, lockWaitAge, asyncCallback);
      },
      // Unmusk password
      function(asyncCallback) {
        _unmaskGitConfig(localPath, scriptMarket, asyncCallback);
      },
      // Get Commit ID
      function(asyncCallback) {
        git.revparse(['HEAD'], function(err, commitId) {
          prevCommitId = commitId || null;

          // Ignore error on empty repo
          return asyncCallback();
        })
      },
      // git reset
      function(asyncCallback) {
        git.reset(simpleGit.ResetMode.HARD, asyncCallback);
      },
      // git clean
      function(asyncCallback) {
        git.clean(simpleGit.CleanOptions.FORCE, asyncCallback);
      },
      // git pull
      function(asyncCallback) {
        if (!prevCommitId) return asyncCallback();

        git.pull(asyncCallback);
      },
    ], function(err) {
      // Mask password
      _maskGitConfig(localPath, scriptMarket, function() {
        // unlock
        locals.cacheDB.unlock(lockKey, lockValue, function() {
          if (err) {
            if (err instanceof simpleGit.GitPluginError && err.plugin === 'timeout') {
              return callback(new E('EClient', 'Accessing git repo timeout'));
            } else {
              return callback(err);
            }
          }

          // Get META
          var metaData = _getMetaData(scriptMarket);
          return callback(null, metaData);
        });
      });
    });
  },
  aliyunOSS: function(locals, scriptMarket, callback) {
    var localPath = _getLocalAbsPath(scriptMarket);

    var lockKey     = toolkit.getCacheKey('lock', 'scriptMarketOperation');
    var lockValue   = toolkit.genRandString();
    var lockAge     = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_AGE;
    var lockWaitAge = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_WAIT_AGE;
    async.series([
      // Lock
      function(asyncCallback) {
        locals.cacheDB.lockWait(lockKey, lockValue, lockAge, lockWaitAge, asyncCallback);
      },
      // Re-download META、TOKEN
      function(asyncCallback) {
        var files = [
          CONFIG._SCRIPT_EXPORT_META_FILE,
          CONFIG._SCRIPT_MARKET_TOKEN_FILE,
        ];
        async.eachSeries(files, function(file, eachCallback) {
          SCRIPT_MARKET_DOWNLOAD_FUNC_MAP[scriptMarket.type](scriptMarket, localPath, file, function(err) {
            // Do not throw download error when Init
            return eachCallback();
          });
        }, asyncCallback);
      },
    ], function(err) {
      // Unlock
      locals.cacheDB.unlock(lockKey, lockValue, function() {
        if (err) return callback(err);

        // Get META
        var metaData = _getMetaData(scriptMarket);
        return callback(null, metaData);
      });
    });
  },
};
SCRIPT_MARKET_RESET_FUNC_MAP.httpService = SCRIPT_MARKET_RESET_FUNC_MAP.aliyunOSS;

// Script Market - Download
var SCRIPT_MARKET_DOWNLOAD_FUNC_MAP = {
  git: function(scriptMarket, localPath, files, callback) {
    // Use git reset in Git
    return callback();
  },
  aliyunOSS: function(scriptMarket, localSavePath, file, callback) {
    var ossFilePath   = `oss://${scriptMarket.configJSON.bucket}/${scriptMarket.configJSON.folder}/${file}`;
    var localFilePath = path.join(localSavePath, file);
    var ossEndpoint   = scriptMarket.configJSON.endpoint;

    // Ensure folder
    fs.ensureDirSync(path.dirname(localFilePath));

    // Remove prev files
    fs.removeSync(localFilePath);

    // Download files
    var cmdArgs = [
      'cp', ossFilePath, localFilePath, '-f',
      '-e', ossEndpoint,
      '-i', scriptMarket.configJSON.accessKeyId,
      '-k', scriptMarket.configJSON.accessKeySecret,
    ]
    if (toolkit.endsWith(file, '/')) cmdArgs.push('-r');

    toolkit.childProcessSpawn(OSSUTIL_CMD, cmdArgs, null, function(err, stdout) {
      if (err) return callback(err);

      if (!fs.existsSync(localFilePath)) {
        return callback(new Error('Fetch file from Aliyun OSS failed.'))
      }

      return callback();
    });
  },
  httpService: function(scriptMarket, localSavePath, file, callback) {
    var fileURL       = `${scriptMarket.configJSON.url}/${file}`;
    var localFilePath = path.join(localSavePath, file);

    // Ensure folder
    fs.ensureDirSync(path.dirname(localFilePath));

    // Remove prev files
    fs.removeSync(localFilePath);

    // Download files
    var requestOptions = {
      forever: true,
      method : 'get',
      url    : fileURL,
    };
    request(requestOptions, function(err, _res, _body) {
      if (err) return callback(err);

      if (_res.statusCode !== 200) {
        return callback(new Error(`Fetch file [${file}] from HTTP Service failed. Server returned a status code [${_res.statusCode} ${_res.statusMessage}]`));
      }

      var fileContent = _body.toString();
      var fileExt = fileURL.split('.').pop();

      try {
        // Verify file format
        switch(fileExt) {
          case 'json':
            JSON.parse(fileContent);
            break;

          case 'yaml':
            yaml.load(fileContent);
            break;
        }

      } catch(err) {
        return callback(new Error(`Fetch file [${file}] from HTTP Service failed. File cannot be load as [${fileExt}] format`))
      }

      fs.outputFileSync(localFilePath, fileContent);

      if (!fs.existsSync(localFilePath)) {
        return callback(new Error(`Fetch file [${file}] from HTTP Service failed`))
      }
      return callback();
    });
  },
}

// Script Market - Upload
var SCRIPT_MARKET_UPLOAD_REPO_FUNC_MAP = {
  git: function(locals, scriptMarket, pushContent, callback) {
    var localPath = _getLocalAbsPath(scriptMarket);
    if (!fs.existsSync(path.join(localPath, '.git/config'))) {
      return callback(new Error('Local git folder is broken, please delete the Script Market and add it again'))
    }

    var git = toolkit.createGitHandler(localPath);

    var prevCommitId = null;

    var lockKey     = toolkit.getCacheKey('lock', 'scriptMarketOperation');
    var lockValue   = toolkit.genRandString();
    var lockAge     = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_AGE;
    var lockWaitAge = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_WAIT_AGE;
    async.series([
      // Lock
      function(asyncCallback) {
        locals.cacheDB.lockWait(lockKey, lockValue, lockAge, lockWaitAge, asyncCallback);
      },
      // Unmask password
      function(asyncCallback) {
        _unmaskGitConfig(localPath, scriptMarket, asyncCallback);
      },
      // Get Commit ID
      function(asyncCallback) {
        git.revparse(['HEAD'], function(err, commitId) {
          prevCommitId = commitId || null;

          // Ignore error on empty repo
          return asyncCallback();
        })
      },
      // git add
      function(asyncCallback) {
        git.add('.', asyncCallback);
      },
      // git commit
      function(asyncCallback) {
        git
        .addConfig('user.name', locals.user.name)
        .addConfig('user.email', locals.user.email)
        .commit(pushContent.extra.note, asyncCallback);
      },
      // git push / reset
      function(asyncCallback) {
        git.push(function(err) {
          if (err) {
            return git.raw(['reset', '--hard', prevCommitId], function() {
              // Ensure error been thrown
              return asyncCallback(err);
            })
          }

          return asyncCallback();
        });
      },
    ], function(err) {
      // Mask password
      _maskGitConfig(localPath, scriptMarket, function() {
        // Unlock
        locals.cacheDB.unlock(lockKey, lockValue, function() {
          if (err) {
            if (err instanceof simpleGit.GitPluginError && err.plugin === 'timeout') {
              return callback(new Error('Accessing git repo timeout'));
            } else {
              return callback(err);
            }
          }
          return callback();
        });
      });
    });
  },
  aliyunOSS: function(locals, scriptMarket, pushContent, callback) {
    if (toolkit.isNothing(pushContent)) return callback();

    var localPath = _getLocalAbsPath(scriptMarket);
    if (!fs.existsSync(localPath)) {
      return callback(new Error('Local data folder not exists'))
    }

    var ossPath     = `oss://${scriptMarket.configJSON.bucket}/${scriptMarket.configJSON.folder}/`;
    var ossEndpoint = scriptMarket.configJSON.endpoint;

    var lockKey     = toolkit.getCacheKey('lock', 'scriptMarketOperation');
    var lockValue   = toolkit.genRandString();
    var lockAge     = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_AGE;
    var lockWaitAge = CONFIG._SCRIPT_MARKET_OPERATION_LOCK_WAIT_AGE;
    async.series([
      // Lock
      function(asyncCallback) {
        locals.cacheDB.lockWait(lockKey, lockValue, lockAge, lockWaitAge, asyncCallback);
      },
      // Upload non-script files
      function(asyncCallback) {
        var files = [
          CONFIG._SCRIPT_EXPORT_META_FILE,
          CONFIG._SCRIPT_MARKET_TOKEN_FILE,
          CONFIG._SCRIPT_MARKET_README_FILE,
        ]
        async.eachSeries(files, function(file, eachCallback) {
          var localFilePath = path.join(localPath, file);
          var ossFilePath   = ossPath + file;

          if (!fs.existsSync(localFilePath)) return eachCallback();

          var cmdArgs = [
            'cp', localFilePath, ossFilePath, '-f',
            '-e', ossEndpoint,
            '-i', scriptMarket.configJSON.accessKeyId,
            '-k', scriptMarket.configJSON.accessKeySecret,
          ];
          if (toolkit.endsWith(file, '/')) cmdArgs.push('-r');

          toolkit.childProcessSpawn(OSSUTIL_CMD, cmdArgs, null, function(err, stdout) {
            if (err) return eachCallback(err);
            return eachCallback();
          });
        }, asyncCallback);
      },
      // Upload Scripts
      function(asyncCallback) {
        if (toolkit.isNothing(pushContent.scriptSets)) return asyncCallback();

        async.eachSeries(pushContent.scriptSets, function(scriptSet, eachCallback) {
          var localFolderPath = path.join(localPath, CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR, scriptSet.id) + '/';
          var ossFolderPath   = ossPath + `${CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR}/${scriptSet.id}/`;
          var cmdArgs = [
            'cp', localFolderPath, ossFolderPath, '-r', '-f',
            '-e', ossEndpoint,
            '-i', scriptMarket.configJSON.accessKeyId,
            '-k', scriptMarket.configJSON.accessKeySecret,
          ];

          toolkit.childProcessSpawn(OSSUTIL_CMD, cmdArgs, null, function(err, stdout) {
            if (err) return eachCallback(err);
            return eachCallback();
          });
        }, asyncCallback);
      },
    ], function(err) {
      // Unlock
      locals.cacheDB.unlock(lockKey, lockValue, function() {
        if (err) return callback(err);
        return callback();
      });
    });
  },
  httpService: function(locals, scriptMarket, pushContent, callback) {
    return callback(new E('EClient', 'Publishing is not supported on HTTP Service'));
  },
};

// Script Market - Get local abs path
function _getLocalAbsPath(scriptMarket) {
  switch(scriptMarket.type) {
    case 'git':
      var localAbsPath = path.join(
          CONFIG.RESOURCE_ROOT_PATH,
          CONFIG._SCRIPT_MARKET_BASE_DIR,
          CONFIG._SCRIPT_MARKET_GIT_REPO_DIR,
          scriptMarket.id);

      return localAbsPath;

    case 'aliyunOSS':
      var localAbsPath = path.join(
          CONFIG.RESOURCE_ROOT_PATH,
          CONFIG._SCRIPT_MARKET_BASE_DIR,
          CONFIG._SCRIPT_MARKET_ALIYUN_OSS_REPO_DIR,
          scriptMarket.id);
      return localAbsPath;

    case 'httpService':
      var localAbsPath = path.join(
          CONFIG.RESOURCE_ROOT_PATH,
          CONFIG._SCRIPT_MARKET_BASE_DIR,
          CONFIG._SCRIPT_MARKET_HTTP_SERVICE_REPO_DIR,
          scriptMarket.id);

      return localAbsPath;
  }
};

// Script Market - Get META data
function _getMetaData(scriptMarket) {
  var localPath = _getLocalAbsPath(scriptMarket);
  var metaData  = {};

  var metaFilePath = path.join(localPath, CONFIG._SCRIPT_EXPORT_META_FILE);
  if (!fs.existsSync(metaFilePath)) return metaData;

  var metaData = toolkit.safeReadFileSync(metaFilePath, 'yaml') || {};

  // [Compatibility] Convert import / export data schema
  metaData = common.convertImportExportDataSchema(metaData);

  return metaData;
};

// Script Market - Set META data
function _writeMetaData(scriptMarket, metaData) {
  var localPath = _getLocalAbsPath(scriptMarket);
  fs.outputFileSync(path.join(localPath, CONFIG._SCRIPT_EXPORT_META_FILE), yaml.dump(metaData));
};

// Script Market - List Script Sets
function _listScriptSets(locals, scriptMarket, callback) {
  var scriptSets = [];
  async.series([
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, function(err, metaData) {
        if (err) return asyncCallback(err);

        if (toolkit.notNothing(metaData)) {
          scriptSets = metaData.scriptSets || [];
        }

        return asyncCallback();
      });
    },
  ], function(err) {
    if (err) return callback(err);
    return callback(null, scriptSets);
  });
}

// Script Market - Set admin
function _setAdmin(locals, scriptMarket, callback) {
  // Error on write operations to read-only Script Market
  if (SCRIPT_MARKET_RW_MAP[scriptMarket.type] !== 'rw') {
    return callback(new E('EBizCondition', 'This operation is not supported on this type of Script Market'));
  }

  async.series([
    // Prepare
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, asyncCallback);
    },
    // Check and set Token
    function(asyncCallback) {
      var token           = _getToken(scriptMarket);
      var remoteTokenInfo = _getRemoteTokenInfo(scriptMarket);

      if (!remoteTokenInfo.value) {
        // Write Token if no admin set
        return fs.outputFile(remoteTokenInfo.path, token, asyncCallback);

      } else if (remoteTokenInfo.value === token) {
        // Skip if already been admin
        return callback();

      } else {
        // Error if already admined by other DataFlux Func
        return asyncCallback(new E('EClient', 'This Script Market has been administered by other DataFlux Func'));
      }
    },
    // Upload
    function(asyncCallback) {
      var pushContent = {
        extra: { note: 'Set Admin' },
      }
      SCRIPT_MARKET_UPLOAD_REPO_FUNC_MAP[scriptMarket.type](locals, scriptMarket, pushContent, function(err) {
        if (err) {
          locals.logger.logError(err);

          return asyncCallback(new E('EClient', 'Failed to setting Admin',  {
            message: CONFIG.MODE === 'dev' ? err.message : toolkit.maskAuthURL(err.message),
          }));
        }
        return asyncCallback();
      });
    },
  ], callback);
};

// Script Market - Unset admin
function _unsetAdmin(locals, scriptMarket, callback) {
  async.series([
    // Prepare
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, asyncCallback);
    },
    // Check and remove Token
    function(asyncCallback) {
      var token           = _getToken(scriptMarket);
      var remoteTokenInfo = _getRemoteTokenInfo(scriptMarket);

      // Error if not admin
      if (!remoteTokenInfo.value || remoteTokenInfo.value !== token) return callback();

      // Remove Token
      return fs.remove(remoteTokenInfo.path, asyncCallback);
    },
    // Upload
    function(asyncCallback) {
      var pushContent = {
        extra: { note: 'Unset Admin' },
      }
      SCRIPT_MARKET_UPLOAD_REPO_FUNC_MAP[scriptMarket.type](locals, scriptMarket, pushContent, asyncCallback);
    },
  ], callback);
};

// Script Market - Set extra in meta
function _setMetaExtra(locals, scriptMarket, callback) {
  // Error on write operations to read-only Script Market
  if (SCRIPT_MARKET_RW_MAP[scriptMarket.type] !== 'rw') {
    return callback(new E('EBizCondition', 'This operation is not supported on this type of Script Market'));
  }

  // Skip if no extra
  if (!scriptMarket.extraJSON) return callback();

  async.series([
    // Prepare
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, asyncCallback);
    },
    // Check and modify Meta.extra
    function(asyncCallback) {
      var metaData = _getMetaData(scriptMarket);

      metaData = metaData || {};
      metaData.extra = metaData.extra || {};

      Object.keys(scriptMarket.extraJSON).forEach(function(f) {
        if (toolkit.isNothing(scriptMarket.extraJSON[f])) {
          metaData.extra[f] = null;
        } else {
          metaData.extra[f] = scriptMarket.extraJSON[f];
        }
      });

      _writeMetaData(scriptMarket, metaData);
      return asyncCallback();
    },
    // Upload
    function(asyncCallback) {
      var pushContent = {
        extra: { note: 'Set Meta Extra' },
      }
      SCRIPT_MARKET_UPLOAD_REPO_FUNC_MAP[scriptMarket.type](locals, scriptMarket, pushContent, function(err) {
        if (err) {
          locals.logger.logError(err);

          return asyncCallback(new E('EClient', 'Failed to setting Admin',  {
            message: CONFIG.MODE === 'dev' ? err.message : toolkit.maskAuthURL(err.message),
          }));
        }
        return asyncCallback();
      });
    },
  ], callback);
};

// Script Market - Push
function _pushToScriptMarket(locals, scriptMarket, pushContent, callback) {
  var localPath = _getLocalAbsPath(scriptMarket);

  var metaData        = null;
  var tmpScriptSetMap = {};
  async.series([
    // Prepare
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, function(err, _metaData) {
        if (err) return asyncCallback(err);

        metaData = _metaData || {};

        // Add extra
        metaData.extra = metaData.extra || {};
        Object.assign(metaData.extra, pushContent.extra || {});

        // Temp Script Set map
        if (toolkit.notNothing(metaData.scriptSets)) {
          tmpScriptSetMap = toolkit.arrayElementMap(metaData.scriptSets, 'id');
        }

        return asyncCallback();
      });
    },
    // Check Token
    function(asyncCallback) {
      var token           = _getToken(scriptMarket);
      var remoteTokenInfo = _getRemoteTokenInfo(scriptMarket);

      // Error if already admined by other DataFlux Func
      if (!remoteTokenInfo.value || remoteTokenInfo.value !== token) {
        return asyncCallback(new E('EClient', 'This DataFlux Func is not admin of the Script Market'));
      }

      return asyncCallback();
    },
    // Write files to push
    function(asyncCallback) {
      try {
        // Remove Script Set (index only)
        if (toolkit.notNothing(pushContent.deleteScriptSetIds)) {
          pushContent.deleteScriptSets = [];

          pushContent.deleteScriptSetIds.forEach(function(scriptSetId) {
            var scriptSetToDelete = tmpScriptSetMap[scriptSetId];
            if (scriptSetToDelete) {
              // Remove index
              delete tmpScriptSetMap[scriptSetId];

              // Record deleted Script Set IDs
              pushContent.deleteScriptSets.push(scriptSetToDelete);
            }
          });
        }

        // Write Script Sets (index + code file)
        if (toolkit.notNothing(pushContent.scriptSets)) {
          pushContent.scriptSets.forEach(function(scriptSet) {
            var scriptSetDir = path.join(localPath, CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR, scriptSet.id);

            // Gen README of Script Set
            var scriptSetReadmeFilePath = path.join(scriptSetDir, CONFIG._SCRIPT_MARKET_README_FILE);
            var scriptSetReadmeData = _isAutoGeneratedFileOrNothing(scriptSetReadmeFilePath)
                                    ? _getDefaultScriptSetReadmeContent(scriptSet)
                                    : toolkit.safeReadFileSync(scriptSetReadmeFilePath);

            // Gen CHANGELOG of Script Set
            var scriptSetChangelogInfoFilePath = path.join(scriptSetDir, CONFIG._SCRIPT_MARKET_CHANGELOG_INFO_FILE);
            var changelogInfoData = toolkit.safeReadFileSync(scriptSetChangelogInfoFilePath, 'yaml') || {};
            changelogInfoData = _addChangelogInfo(changelogInfoData, scriptSet);

            var scriptSetChangelogFilePath = path.join(scriptSetDir, CONFIG._SCRIPT_MARKET_CHANGELOG_FILE);
            var scriptSetChangelogData = _isAutoGeneratedFileOrNothing(scriptSetChangelogFilePath)
                                    ? _getDefaultScriptSetChangelogContent(changelogInfoData)
                                    : toolkit.safeReadFileSync(scriptSetChangelogFilePath);

            // Prepare folder of Script Set
            fs.emptyDirSync(scriptSetDir);

            // Write README
            fs.outputFileSync(scriptSetReadmeFilePath, scriptSetReadmeData);

            // Write CHANGELOG
            fs.outputFileSync(scriptSetChangelogInfoFilePath, _addAutoGeneratedInfo(yaml.dump(changelogInfoData), 'yaml'));
            fs.outputFileSync(scriptSetChangelogFilePath, scriptSetChangelogData);

            // Write code files
            if (toolkit.notNothing(scriptSet.scripts)) {
              scriptSet.scripts.forEach(function(script) {
                var filePath = path.join(scriptSetDir, common.getScriptFilename(script));
                fs.outputFileSync(filePath, script.code || '');

                // No code field in META
                delete script.code;
              });
            }

            // Add to index
            tmpScriptSetMap[scriptSet.id] = scriptSet;
          });
        }

        // Re-gen and write META
        metaData.scriptSets = Object.values(tmpScriptSetMap);
        _writeMetaData(scriptMarket, metaData);

        // Write README of Script Market
        var scriptMarketReadmeFilePath = path.join(localPath, CONFIG._SCRIPT_MARKET_README_FILE);
        if (_isAutoGeneratedFileOrNothing(scriptMarketReadmeFilePath)) {
          var scriptMarketReadmeData = _getDefaultScriptMarketReadmeContent(scriptMarket, pushContent);
          fs.outputFileSync(scriptMarketReadmeFilePath, scriptMarketReadmeData);
        }

        return asyncCallback();

      } catch(err) {
        return asyncCallback(err);
      }
    },
    // Upload
    function(asyncCallback) {
      SCRIPT_MARKET_UPLOAD_REPO_FUNC_MAP[scriptMarket.type](locals, scriptMarket, pushContent, asyncCallback);
    },
  ], callback);
};

// Script Market - Pull
function _pullFromScriptMarket(locals, scriptMarket, pullScriptSetIds, callback) {
  if (toolkit.isNothing(pullScriptSetIds)) {
    return callback(new E('EClient', 'Nothing to pull'));
  }

  var localPath = _getLocalAbsPath(scriptMarket);

  var pullContent = {
    scriptSets: [],
  };

  var metaData        = null;
  var tmpScriptSetMap = null;
  async.series([
    // Prepare
    function(asyncCallback) {
      SCRIPT_MARKET_RESET_FUNC_MAP[scriptMarket.type](locals, scriptMarket, function(err, _metaData) {
        if (err) return asyncCallback(err);

        metaData        = _metaData || {};
        tmpScriptSetMap = toolkit.arrayElementMap(metaData.scriptSets || [], 'id');

        return asyncCallback();
      });
    },
    // Download contents
    function(asyncCallback) {
      var files = [];

      pullScriptSetIds.forEach(function(scriptSetId) {
        var scriptSet = tmpScriptSetMap[scriptSetId];

        if (toolkit.isNothing(scriptSet))         return;
        if (toolkit.isNothing(scriptSet.scripts)) return;

        scriptSet.scripts.forEach(function(script) {
          var codeFilePath = path.join(CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR, scriptSetId, common.getScriptFilename(script));
          files.push(codeFilePath);
        })
      });

      async.eachLimit(files, 5, function(file, eachCallback) {
        SCRIPT_MARKET_DOWNLOAD_FUNC_MAP[scriptMarket.type](scriptMarket, localPath, file, eachCallback);
      }, asyncCallback);
    },
    // Gen content for importing
    function(asyncCallback) {
      pullScriptSetIds.forEach(function(scriptSetId) {
        // Read coode
        var scriptSet = tmpScriptSetMap[scriptSetId];
        scriptSet.scripts.forEach(function(script) {
          var codeFilePath = path.join(localPath, CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR, scriptSetId, common.getScriptFilename(script));
          script.code = toolkit.safeReadFileSync(codeFilePath);
        });

        pullContent.scriptSets.push(scriptSet);
      });

      return asyncCallback();
    },
  ], function(err) {
    if (err) return callback(err);
    return callback(null, pullContent);
  });
};

/* Handlers */
var crudHandler = exports.crudHandler = scriptMarketMod.createCRUDHandler();

exports.list = function(req, res, next) {
  var listData     = null;
  var listPageInfo = null;

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  var scriptMarketModelForFetch = scriptMarketMod.createModel(res.locals);
  scriptMarketModelForFetch.decipher = true;

  async.series([
    // Get Script Market list
    function(asyncCallback) {
      var opt = res.locals.getQueryOptions();

      scriptMarketModel.list(opt, function(err, dbRes, pageInfo) {
        if (err) return asyncCallback(err);

        listData     = dbRes;
        listPageInfo = pageInfo;

        return asyncCallback();
      });
    },
  ], function(err) {
    if (err) return next(err);

    // Get admin, Script Set META
    listData.forEach(function(scriptMarket) {
      // Get META
      var metaData = _getMetaData(scriptMarket);

      // Script Set, extra
      scriptMarket.scriptSets = metaData.scriptSets || [];
      scriptMarket.extra      = metaData.extra      || {};

      // Check if is admin
      var token       = _getToken(scriptMarket);
      var remoteToken = _getRemoteTokenInfo(scriptMarket).value;
      scriptMarket.isAdmin = (token === remoteToken);
    });

    var ret = toolkit.initRet(listData, listPageInfo);
    res.locals.sendJSON(ret);
  });
};

exports.add = function(req, res, next) {
  var setAdmin = toolkit.toBoolean(req.body.setAdmin);
  var data     = _prepareConfig(req.body.data);

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);

  // Pre-gen ID for TOKEN
  data.id = scriptMarketModel.genDataId();

  var newScriptMarket = null;

  async.series([
    // Check config of Script Market
    function(asyncCallback) {
      if (toolkit.isNothing(data.configJSON)) return asyncCallback();

      return _checkConfig(res.locals, data, asyncCallback);
    },
    // Init Script Market
    function(asyncCallback) {
      SCRIPT_MARKET_INIT_FUNC_MAP[data.type](res.locals, data, setAdmin, asyncCallback);
    },
    // Set Admin
    function(asyncCallback) {
      if (!setAdmin) return asyncCallback();

      return _setAdmin(res.locals, data, asyncCallback);
    },
    // Set Extra
    function(asyncCallback) {
      if (!setAdmin) return asyncCallback();

      return _setMetaExtra(res.locals, data, asyncCallback);
    },
    // Save to DB
    function(asyncCallback) {
      // Fill extra
      var extra = _getMetaData(data).extra || {};
      data.extraJSON = {};
      for (var f in extra) {
        if (EXPORT_FIELDS.indexOf(f) >= 0) continue;
        data.extraJSON[f] = extra[f];
      }

      scriptMarketModel.add(data, function(err, _addedId, _addedData) {
        if (err) return asyncCallback(err);

        newScriptMarket = _addedData;

        return asyncCallback();
      });
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet({
      id: newScriptMarket.id,
    });
    return res.locals.sendJSON(ret);
  });
};

exports.addOfficial = function(req, res, next) {
  var data = {
    id        : CONFIG._OFFICIAL_SCRIPT_MARKET_ID,
    type      : 'httpService',
    configJSON: { "url": CONFIG._OFFICIAL_SCRIPT_MARKET_URL },
    isPinned  : true
  };

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);

  async.series([
    // Init Script Market
    function(asyncCallback) {
      SCRIPT_MARKET_INIT_FUNC_MAP[data.type](res.locals, data, false, asyncCallback);
    },
    // Save to DB
    function(asyncCallback) {
      scriptMarketModel.add(data, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet({
      id: data.id,
    });
    return res.locals.sendJSON(ret);
  });
};

exports.modify = function(req, res, next) {
  var id   = req.params.id;
  var data = _prepareConfig(req.body.data);

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptMarket = null;

  async.series([
    // Get Script Market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.ModifyingScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        return asyncCallback();
      });
    },
    // Check config of Script Market
    function(asyncCallback) {
      if (toolkit.isNothing(data.configJSON)) return asyncCallback();

      return _checkConfig(res.locals, data, asyncCallback);
    },
    // Init Script Market
    function(asyncCallback) {
      if (toolkit.isNothing(data.configJSON)) return asyncCallback();

      scriptMarket.configJSON = data.configJSON;
      SCRIPT_MARKET_INIT_FUNC_MAP[scriptMarket.type](res.locals, scriptMarket, false, asyncCallback);
    },
    // Set extra
    function(asyncCallback) {
      if (toolkit.isNothing(data.extraJSON)) return asyncCallback();

      // Check if is admin or not
      var token       = _getToken(scriptMarket);
      var remoteToken = _getRemoteTokenInfo(scriptMarket).value;
      if (token !== remoteToken) return asyncCallback();

      // Merge extra
      scriptMarket.extraJSON = scriptMarket.extraJSON || {};
      for (var k in scriptMarket.extraJSON) {
        switch(k) {
          // i18n
          case 'i18n':
            for (var lang in scriptMarket.extraJSON.i18n) {
              data.extraJSON.i18n = data.extraJSON.i18n || {};
              if (!data.extraJSON.i18n[lang]) continue;

              for (var _text in scriptMarket.extraJSON.i18n) {
                if (!(_text in data.extraJSON.i18n[lang])) continue;
                data.extraJSON.i18n[lang][_text] = data.extraJSON.i18n[lang][_text] || scriptMarket.extraJSON.i18n[lang][_text];
              }
            }
            break;

          default:
            data.extraJSON[k] = data.extraJSON[k] || scriptMarket.extraJSON[k];
            break;
        }
      }

      scriptMarket.extraJSON = data.extraJSON;

      return _setMetaExtra(res.locals, scriptMarket, asyncCallback);
    },
    // Save to DB
    function(asyncCallback) {
      // Lock status / config
      if (data.isLocked === true) {
        data.lockedByUserId = res.locals.user.id;
        data.lockConfigJSON = data.lockConfigJSON || []
      } else if (data.isLocked === false) {
        data.lockedByUserId = null;
        data.lockConfigJSON = null;
      }
      delete data.isLocked;

      scriptMarketModel.modify(id, data, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet({
      id: id,
    });
    return res.locals.sendJSON(ret);
  });
};

exports.delete = function(req, res, next) {
  var id = req.params.id;

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptMarket = null;

  async.series([
    // Get Script Market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.DeletingScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        // Skip if not rw repo
        if (SCRIPT_MARKET_RW_MAP[scriptMarket.type] !== 'rw') return asyncCallback();

        // Skip if user / password is missing
        switch(scriptMarket.type) {
          case 'git':
            if (!scriptMarket.configJSON
                || !scriptMarket.configJSON.user
                || !scriptMarket.configJSON.password) {
              return asyncCallback();
            }
            break;
        }

        // Unset admin (errors will be ignored)
        _unsetAdmin(res.locals, scriptMarket, function(err) {
          if (err) res.locals.logger.error(err.toString());
        });

        return asyncCallback();
      });
    },
    // Save to DB
    function(asyncCallback) {
      scriptMarketModel.delete(id, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet({
      id: id,
    });
    return res.locals.sendJSON(ret);
  });
};

exports.listScriptSets = function(req, res, next) {
  var id = req.params.id;

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptSets   = [];
  var scriptMarket = null;
  async.series([
    // Get Script market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        return asyncCallback();
      })
    },
    // List Script Sets
    function(asyncCallback) {
      _listScriptSets(res.locals, scriptMarket, function(err, _scriptSets) {
        if (err) {
          return asyncCallback(new E('EBizCondition.FetchScriptMarketFailed', 'Fetch Script Market Script Set list Failed', {
            message: err.toString(),
          }));
        }

        scriptSets = _scriptSets;

        return asyncCallback();
      });
    },
    // Load example file code
    function(asyncCallback) {
      if (toolkit.isNothing(scriptSets)) return asyncCallback();

      var localPath = _getLocalAbsPath(scriptMarket);
      async.eachLimit(scriptSets, 5, function(scriptSet, eachScriptSetCallback) {
        if (toolkit.isNothing(scriptSet.scripts)) return eachScriptSetCallback();

        async.eachLimit(scriptSet.scripts, 5, function(script, eachScriptCallback) {
          if (script.id !== `${scriptSet.id}__example`) return eachScriptCallback();

          var file = path.join(CONFIG._SCRIPT_EXPORT_SCRIPT_SET_DIR, scriptSet.id, common.getScriptFilename(script));
          SCRIPT_MARKET_DOWNLOAD_FUNC_MAP[scriptMarket.type](scriptMarket, localPath, file, function(err) {
            if (err) return eachScriptCallback(err);

            var codeFilePath = path.join(localPath, file);
            script.code = toolkit.safeReadFileSync(codeFilePath) || '';

            return eachScriptCallback();
          });
        }, eachScriptSetCallback);
      }, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet(scriptSets);
    return res.locals.sendJSON(ret);
  });
};

exports.setAdmin = function(req, res, next) {
  var id = req.params.id;

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptMarket = null;
  async.series([
    // Get Script market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.SettingScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        return asyncCallback();
      })
    },
    // Set admin
    function(asyncCallback) {
      return _setAdmin(res.locals, scriptMarket, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet();
    return res.locals.sendJSON(ret);
  });
};

exports.unsetAdmin = function(req, res, next) {
  var id = req.params.id;

  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptMarket = null;
  async.series([
    // Get Script market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.SettingScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        // Error on write operations to read-only Script Market
        if (SCRIPT_MARKET_RW_MAP[scriptMarket.type] !== 'rw') {
          return asyncCallback(new E('EBizCondition', 'This operation is not supported on this type of Script Market'));
        }

        return asyncCallback();
      })
    },
    // Unset admin
    function(asyncCallback) {
      return _unsetAdmin(res.locals, scriptMarket, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet();
    return res.locals.sendJSON(ret);
  });
};

exports.publish = function(req, res, next) {
  var id           = req.params.id;
  var scriptSetIds = req.body.scriptSetIds;
  var mode         = req.body.mode;
  var note         = req.body.note;

  var scriptSetModel    = scriptSetMod.createModel(res.locals);
  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var scriptMarket = null;
  var pushContent  = null;

  async.series([
    // Get Script market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.PublishingScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        // Error on write operations to read-only Script Market
        if (SCRIPT_MARKET_RW_MAP[scriptMarket.type] !== 'rw') {
          return asyncCallback(new E('EBizCondition', 'This operation is not supported on this type of Script Market'));
        }

        return asyncCallback();
      })
    },
    // Get data for exporting
    function(asyncCallback) {
      if (mode === 'add') {
        // Add mode
        var opt = {
          scriptSetIds: scriptSetIds,
          note        : note,
        }
        scriptSetModel.getExportData(opt, function(err, exportData) {
          if (err) return asyncCallback(err);

          pushContent = exportData;

          return asyncCallback();
        });

      } else {
        // Remove mode
        pushContent = {
          deleteScriptSetIds: scriptSetIds,
          extra: {
            exportUser: common.getExportUser(res.locals),
            exportTime: toolkit.getISO8601(),
            note      : note,
          }
        }
        return asyncCallback();
      }
    },
    // Push data
    function(asyncCallback) {
      _pushToScriptMarket(res.locals, scriptMarket, pushContent, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet();
    return res.locals.sendJSON(ret);
  });
};

exports.install = function(req, res, next) {
  var id            = req.params.id;
  var scriptSetIds  = req.body.scriptSetIds;
  var deployOptions = req.body.deployOptions || {};

  if (toolkit.isNothing(scriptSetIds)) {
    return new E('EBizCondition', 'Script Set ID not specified');
  }

  var scriptSetModel    = scriptSetMod.createModel(res.locals);
  var scriptModel       = scriptMod.createModel(res.locals);
  var funcModel         = funcMod.createModel(res.locals);
  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  var cronJobModel      = cronJobMod.createModel(res.locals);

  scriptMarketModel.decipher = true;

  var scriptMarket            = null;
  var availableScriptSetIdMap = {};

  var importData       = {};
  var requirements     = null;
  var exampleScriptIds = [];
  var configFields     = [];

  var startupScriptIds            = [];
  var startupCronJobIds           = [];
  var startupScriptCronJobFuncMap = {};

  async.series([
    // Get Script market
    function(asyncCallback) {
      scriptMarketModel.getWithCheck(id, null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarket = dbRes;

        // Check lock config
        if (!res.locals.user.is('sa')) {
          // No limitation to sa user
          if (scriptMarket.lockedByUserId && scriptMarket.lockedByUserId !== res.locals.user.id) {
            return asyncCallback(new E('EBizCondition.InstallingScriptSetFromScriptMarketNotAllowed', 'This Script Market is locked by other'));
          }
        }

        return asyncCallback();
      })
    },
    // Get all available Script Set IDs
    function(asyncCallback) {
      _listScriptSets(res.locals, scriptMarket, function(err, scriptSets) {
        if (err) return asyncCallback(err);

        availableScriptSetIdMap = scriptSets.reduce(function(acc, x) {
          acc[x.id] = true;
          return acc
        }, {})

        return asyncCallback(err);
      });
    },
    // Pull data
    function(asyncCallback) {
      var importedScriptSetIds = [];

      var importScriptSetIds = toolkit.jsonCopy(scriptSetIds);
      async.whilst(
        // Check if more Script Set need to be installed
        function() {
          return importScriptSetIds.length > 0;
        },
        // Import more Script Sets
        function(whileCallback) {
          _pullFromScriptMarket(res.locals, scriptMarket, importScriptSetIds, function(err, _importData) {
            if (err) return whileCallback(err);

            // Merge Script Set data
            for (var k in _importData) {
              if (!importData[k]) {
                importData[k] = _importData[k];
              } else {
                importData[k] = importData[k].concat(_importData[k]);
              }
            }

            // Record Script Set ID
            _importData.scriptSets.forEach(function(scriptSet) {
              importedScriptSetIds.push(scriptSet.id);
            });

            // Search Script Set ID that dependent on
            var nextImportScriptSetIdMap = {};
            _importData.scriptSets.forEach(function(scriptSet) {
              scriptSet.scripts.forEach(function(s) {
                var code = s.code;

                // Remove Python comment lines
                code = code.replace(/^\s*#\s*.*$/gm, '');

                // Add all imported Script Set IDs
                var m = s.code.match(/(?<=import\s+|from\s+)[a-zA-Z0-9_]+(?=__[a-zA-Z0-9_]+)/g);
                if (m) {
                  m.forEach(function(id) {
                    nextImportScriptSetIdMap[id] = true;
                  })
                }
              });
            });

            // Ignore already imported Script Set IDs
            importedScriptSetIds.forEach(function(id) {
              delete nextImportScriptSetIdMap[id];
            });

            // Ignore not exists
            for (var id in nextImportScriptSetIdMap) {
              if (!availableScriptSetIdMap[id]) {
                delete nextImportScriptSetIdMap[id];
              }
            }

            importScriptSetIds = Object.keys(nextImportScriptSetIdMap);

            return whileCallback();
          });
        },
      // Finished, import data
      function(err) {
        if (err) return asyncCallback(err);
        if (toolkit.isNothing(importData)) return asyncCallback(err);

        // Replace origin, originId
        common.replaceImportDataOrigin(importData, 'scriptMarket', scriptMarket.id);

        // No extra Recover Point for auto-installing required Script Sets
        var recoverPoint = {
          type: 'install',
          note: 'System: Before installing Script Sets',
        };
        scriptSetModel.import(importData, recoverPoint, function(err, _requirements) {
          if (err) return asyncCallback(err);

          // 3rd party requirements
          requirements = _requirements;

          // Pick Script ID of example / config map
          var deployScriptSetIdMap = scriptSetIds.reduce(function(acc, x) {
            acc[x] = true;
            return acc;
          }, {});

          importData.scripts.forEach(function(s) {
            if (!deployScriptSetIdMap[s.scriptSetId]) return;

            if (s.id === `${s.scriptSetId}__example`) {
              // Script ID of example
              exampleScriptIds.push(s.id);

              // Cofig placeholder of example
              var m = s.code.match(/"<.+>"/g);
              if (m) {
                m.forEach(function(placeholder) {
                  configFields.push(placeholder.slice(2, -2));
                });
              }
            }
          });

          return asyncCallback();
        });
      });
    },
    // Auto-create startup Script
    function(asyncCallback) {
      if (!deployOptions || !deployOptions.withStartupScript) return asyncCallback();

      async.eachSeries(scriptSetIds, function(scriptSetId, eachCallback) {
        scriptSetAPICtrl.doDeploy(res.locals, scriptSetId, deployOptions, eachCallback);
      }, asyncCallback);
    },
    // Get ID list of startup Scripts
    function(asyncCallback) {
      var ids = scriptSetIds.reduce(function(acc, x) {
        acc.push(`${CONFIG._STARTUP_SCRIPT_SET_ID}__${x}`);
        return acc;
      }, []);
      if (toolkit.isNothing(ids)) return asyncCallback();

      var opt = {
        fields: [ 'scpt.id' ],
        filters: {
          'scpt.id': { in: ids }
        }
      }
      scriptModel.list(opt, function(err, dbRes) {
        if (err) return asyncCallback(err);

        dbRes.forEach(function(d) {
          startupScriptIds.push(d.id);
        })

        return asyncCallback();
      })
    },
    // Get Func list with `fixedCronExpr`
    function(asyncCallback) {
      if (toolkit.isNothing(startupScriptIds)) return asyncCallback();

      var opt = {
        fields: [
          'func.id',
          'func.scriptId',
          'func.extraConfigJSON',
        ],
        filters: {
          'scpt.id': { in: startupScriptIds },
        }
      }
      funcModel.list(opt, function(err, dbRes) {
        if (err) return asyncCallback(err);

        dbRes.forEach(function(d) {
          if (startupScriptCronJobFuncMap[d.scriptId]) return;
          if (!d.extraConfigJSON || !d.extraConfigJSON.fixedCronExpr) return;

          startupScriptCronJobFuncMap[d.scriptId] = d;
        });

        return asyncCallback();
      })
    },
    // Get Cron Job IDs
    function(asyncCallback) {
      if (toolkit.isNothing(startupScriptIds)) return asyncCallback();

      var opt = {
        fields: [ 'cron.id' ],
        filters: {
          'scpt.id': { in: startupScriptIds }
        }
      }
      cronJobModel.list(opt, function(err, dbRes) {
        if (err) return asyncCallback(err);

        dbRes.forEach(function(d) {
          startupCronJobIds.push(d.id);
        })

        return asyncCallback();
      })
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet({
      requirements    : requirements,
      exampleScriptIds: exampleScriptIds,
      configFields    : configFields,

      startupScriptIds           : startupScriptIds,
      startupCronJobIds          : startupCronJobIds,
      startupScriptCronJobFuncMap: startupScriptCronJobFuncMap,
    });
    return res.locals.sendJSON(ret);
  });
};

exports.checkUpdate = function(req, res, next) {
  var scriptMarketId = req.query.scriptMarketId;

  var scriptSetModel    = scriptSetMod.createModel(res.locals);
  var scriptMarketModel = scriptMarketMod.createModel(res.locals);
  scriptMarketModel.decipher = true;

  var localScriptSetMap  = null;
  var remoteScriptSetMap = {};

  var scriptMarketIds = null;
  var scriptMarkets   = null;

  var checkUpdateResult = [];

  async.series([
    // Get local Script Sets (related to Script Market only)
    function(asyncCallback) {
      var fields = common.getScriptSetMD5Fields('sset');
      fields.push('sset.originId');
      fields.push('sset.originMD5');

      var opt = {
        fields: fields,
        filters: {
          'sset.origin': { eq: 'scriptMarket' },
        }
      }

      if (scriptMarketId) {
        opt.filters['sset.originId'] = { eq: scriptMarketId };
      }

      scriptSetModel.list(opt, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarketIds = toolkit.arrayElementValues(dbRes, 'originId', true);
        localScriptSetMap = dbRes.reduce(function(acc, x) {
          var keyObj = { scriptMarketId: x.originId, scriptSetId: x.id };
          var key    = sortedJSON.sortify(keyObj, { stringify: true });
          acc[key] = x;
          return acc;
        }, {});

        return asyncCallback();
      })
    },
    // Get Script market (related to Script Market only)
    function(asyncCallback) {
      scriptMarketModel.list(null, function(err, dbRes) {
        if (err) return asyncCallback(err);

        scriptMarkets = dbRes;

        return asyncCallback();
      });
    },
    // Get remote Script Sets
    function(asyncCallback) {
      if (toolkit.isNothing(scriptMarkets)) return asyncCallback();

      async.eachLimit(scriptMarkets, 5, function(scriptMarket, eachCallback) {
        _listScriptSets(res.locals, scriptMarket, function(err, _scriptSets) {
          // Ignore error iwhen checking update error
          if (err) {
            res.locals.logger.logError(err);
            return eachCallback();
          }

          if (toolkit.notNothing(_scriptSets)) {
            _scriptSets.forEach(function(_scriptSet) {
              var keyObj = { scriptMarketId: scriptMarket.id, scriptSetId: _scriptSet.id };
              var key    = sortedJSON.sortify(keyObj, { stringify: true });
              remoteScriptSetMap[key] = _scriptSet;
            });
          }
          return eachCallback();

        });
      }, asyncCallback);
    },
    // Compare MD5 between local and remote Script Sets
    function(asyncCallback) {
      if (toolkit.isNothing(remoteScriptSetMap)) return asyncCallback();

      async.eachOfSeries(remoteScriptSetMap, function(remoteScriptSet, key, eachCallback) {
        var localScriptSet = localScriptSetMap[key];

        if (!localScriptSet) return eachCallback();

        var keyObj = JSON.parse(key);
        keyObj.localMD5 =  localScriptSet.originMD5;
        keyObj.remoteMD5 = remoteScriptSet.originMD5;
        if (keyObj.localMD5 !== keyObj.remoteMD5) {
          // MD5 of Script Sets not match, add to update result
          checkUpdateResult.push(keyObj);
        }

        return eachCallback();

      }, asyncCallback);
    },
  ], function(err) {
    if (err) return next(err);

    var ret = toolkit.initRet(checkUpdateResult);
    return res.locals.sendJSON(ret);
  });
};
